﻿/* Copyright 2007-2008 dnAnalytics Project.
 *
 * Contributors to this file:
 * Jurgen Van Gael
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 * * Redistributions of source code must retain the above copyright notice, this 
 *   list of conditions and the following disclaimer.
 * * Redistributions in binary form must reproduce the above copyright notice, 
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * * Neither the name of the dnAnalytics Project nor the names of its contributors
 *   may be used to endorse or promote products derived from this software without
 *   specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
using System;
using System.Collections.Generic;

namespace dnAnalytics.Statistics
{
    /// <summary>
    /// Computes a histogram of a particular data set. The histogram is represented by bin counts. A datapoint that
    /// happens to land on a boundary bin will be put in the larger of the two bins, except if it equals the histogram
    /// upper bound.
    /// </summary>
    public class Histogram
    {
        // The array of histogram counts.
        private double[] mBins;

        // The minimum of the data.
        private double mMin;

        // The maximum of the data.
        private double mMax;

        // The histogram bin width.
        private double mBinWidth;

        /// <summary>
        /// Constructs a Histogram specifying the number of bins exactly. The upper and lower bound of the histogram
        /// will be set to the smallest and larger datapoint.
        /// </summary>
        /// <param name="data">The datasequence to build a histogram on.</param>
        /// <param name="nbins">The number of bins to use.</param>
        public Histogram(IEnumerable<double> data, int nbins)
        {
            if (nbins < 1)
            {
                throw new ArgumentOutOfRangeException("The number of bins in a histogram should be at least 1.");
            }

            mMin = System.Double.PositiveInfinity;
            mMax = System.Double.NegativeInfinity;
            mBins = new double[nbins];

            // Determine the minimum and maximum of the data range.
            foreach (double d in data)
            {
                if (d > mMax) { mMax = d; }
                if (d < mMin) { mMin = d; }
            }

            Update(data);
        }

        /// <summary>
        /// Constructs a Histogram specifying the number of bins exactly.
        /// </summary>
        /// <param name="data">The datasequence to build a histogram on.</param>
        /// <param name="nbins">The number of bins to use.</param>
        /// <param name="lower">The histogram lower bound.</param>
        /// <param name="upper">The histogram upper bound.</param>
        public Histogram(IEnumerable<double> data, int nbins, double lower, double upper)
        {
            if (lower > upper)
            {
                throw new ArgumentOutOfRangeException("The histogram lowerbound must be smaller than the upper bound.");
            }
            if (nbins < 1)
            {
                throw new ArgumentOutOfRangeException("The number of bins in a histogram should be at least 1.");
            }

            mMin = lower;
            mMax = upper;
            mBins = new double[nbins];

            Update(data);
        }

        /// <summary>
        /// The number of bins in the histogram.
        /// </summary>
        public int BinCount
        {
            get
            {
                return mBins.Length;
            }
        }

        /// <summary>
        /// Returns the number of elements in a histogram bin.
        /// </summary>
        /// <param name="b">The bin index.</param>
        public double this[int b]
        {
            get { return mBins[b]; }
        }

        /// <summary>
        /// Returns the lower bound of the histogram.
        /// </summary>
        public double LowerBound
        {
            get { return mMin; }
        }

        /// <summary>
        /// Returns the upper bound of the histogram.
        /// </summary>
        public double UpperBound
        {
            get { return mMax; }
        }

        /// <summary>
        /// Increment a bin count by a specified amount.
        /// </summary>
        /// <param name="bidx">The bin index.</param>
        /// <param name="val">The amount to increment the bin count with.</param>
        public void Increment(int bidx, double val)
        {
            mBins[bidx] += val;
        }

        /// <summary>
        /// Computes the bin index for datapoint <paramref name="x"/>.
        /// </summary>
        public int FindBin(double x)
        {
            if (x < mMin)
            {
                throw new ArgumentOutOfRangeException("Datapoint is smaller than the histogram lower bound.");
            }
            else if (x > mMax)
            {
                throw new ArgumentOutOfRangeException("Datapoint is larger than the histogram upper bound.");
            }
            else if (x == mMax)
            {
                return mBins.Length - 1;
            }
            else
            {
                return (int)System.Math.Floor((x - mMin) / mBinWidth);
            }
        }

        /// <summary>
        /// Add data to the histogram bins.
        /// </summary>
        private void Update(IEnumerable<double> data)
        {
            mBinWidth = (UpperBound - LowerBound) / mBins.Length;

            // Add the data to the bins.
            foreach (double d in data)
            {
                mBins[FindBin(d)] += 1.0;
            }
        }
    }
}
